#include <qfile.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qregexp.h>

#include <cmath>

#include <mythtv/mythcontext.h>
#include <mythtv/mythdbcon.h>

#include "globals.h"
#include "metadata.h"
#include "metadatalistmanager.h"
#include "dbaccess.h"
#include "programmeutils.h"

struct SortData
{
    SortData(const QString &title, const QString &filename, const QString &id) :
        m_title(title), m_filename(filename), m_id(id)
    {
    }

    QString m_title;
    QString m_filename;
    QString m_id;
};

bool operator<(const SortData &lhs, const SortData &rhs)
{
    int ret = QString::localeAwareCompare(lhs.m_title, rhs.m_title);

    if (ret == 0)
        ret = QString::localeAwareCompare(lhs.m_filename, rhs.m_filename);

    if (ret == 0)
        ret = QString::localeAwareCompare(lhs.m_id, rhs.m_id);

    return ret < 0;
}

Metadata::SortKey::SortKey() : m_sd(0)
{
}

Metadata::SortKey::SortKey(const SortData &data)
{
    m_sd = new SortData(data);
}

Metadata::SortKey::SortKey(const SortKey &other)
{
    *this = other;
}

Metadata::SortKey &Metadata::SortKey::operator=(const SortKey &rhs)
{
    if (this != &rhs)
    {
        Clear();
        if (rhs.m_sd)
            m_sd = new SortData(*rhs.m_sd);
    }

    return *this;
}

Metadata::SortKey::~SortKey()
{
    Clear();
}

bool Metadata::SortKey::isSet() const
{
    return m_sd != 0;
}

void Metadata::SortKey::Clear()
{
    delete m_sd;
    m_sd = 0;
}

class MetadataImp
{
  public:
    typedef Metadata::genre_list genre_list;


  public:
    MetadataImp(
              const QString &url,
              const QString &filename,
              const QString &coverfile,
              const QString &title,
              const QString &inetref,
              const QString &description,
              const QString &series,
              float userrating,
              const QString &rating,
              int id,
              ParentalLevel::Level showlevel,
              const QString &playcommand,
              const genre_list &genres ) :
        m_url(url),
        m_title(title),
        m_inetref(inetref), m_series(series), m_description(description),
        m_rating(rating), m_playcommand(playcommand),
        m_genres(genres),
        m_filename(filename), m_coverfile(coverfile),
        m_showlevel(showlevel), m_id(id),
        m_userrating(userrating)
    {

    }

    MetadataImp(MSqlQuery &query)
    {
        fromDBRow(query);
    }

    MetadataImp(const MetadataImp &other)
    {
        if (this != &other)
        {
            *this = other;
        }
    }

    MetadataImp &operator=(const MetadataImp &rhs)
    {
        m_url = rhs.m_url;
        m_title = rhs.m_title;
        m_inetref = rhs.m_inetref;
        m_series = rhs.m_series;
        m_description = rhs.m_description;
        m_rating = rhs.m_rating;
        m_playcommand = rhs.m_playcommand;
        m_genres = rhs.m_genres;
        m_filename = rhs.m_filename;
        m_coverfile = rhs.m_coverfile;

        m_showlevel = rhs.m_showlevel;
        m_id = rhs.m_id;
        m_userrating = rhs.m_userrating;

        // No DB vars
        m_sort_key = rhs.m_sort_key;
        m_prefix = rhs.m_prefix;
        m_flat_index = rhs.m_flat_index;

        return *this;
    }

  public:
    bool hasSortKey() const { return m_sort_key.isSet(); }
    const Metadata::SortKey &getSortKey() const { return m_sort_key; }
    void setSortKey(const Metadata::SortKey &sort_key)
    {
        m_sort_key = sort_key;
    }

    void setFlatIndex(int index) { m_flat_index = index; }
    int getFlatIndex() const { return m_flat_index; }

    const QString &getPrefix() const { return m_prefix; }
    void setPrefix(const QString &prefix) { m_prefix = prefix; }

    const QString &getURL() const { return m_url; }
    void setURL(const QString &url) { m_url = url; }

    const QString &getTitle() const { return m_title; }
    void setTitle(const QString& title)
    {
        m_sort_key.Clear();
        m_title = title;
    }

    const QString &getInetRef() const { return m_inetref; }
    void setInetRef(const QString &inetRef) { m_inetref = inetRef; }

    const QString &getSeries() const { return m_series; }
    void setSeries(const QString &series) { m_series = series; }

    const QString &getDescription() const { return m_description; }
    void setDescription(const QString &description) { m_description = description; }

    const QString &getRating() const { return m_rating; }
    void setRating(const QString &rating) { m_rating = rating; }

    const QString &getPlayCommand() const { return m_playcommand; }
    void setPlayCommand(const QString &playCommand)
    {
        m_playcommand = playCommand;
    }

    const genre_list &getGenres() const { return m_genres; }
    void setGenres(const genre_list &genres) { m_genres = genres; }

    const QString &getFilename() const { return m_filename; }
    void setFilename(const QString &filename) { m_filename = filename; }

    QString getFilenameNoPrefix() const
    {
        QString ret(m_filename);
        if (ret.startsWith(m_prefix + "/"))
        {
            ret.remove(0, m_prefix.length() + 1);
        }
        else if (ret.startsWith(m_prefix))
        {
            ret.remove(0, m_prefix.length());
        }

        return ret;
    }

    const QString &getCoverFile() const { return m_coverfile; }
    void setCoverFile(const QString &coverFile) { m_coverfile = coverFile; }

    ParentalLevel::Level getShowLevel() const { return m_showlevel; }
    void setShowLevel(ParentalLevel::Level showLevel)
    {
        m_showlevel = ParentalLevel(showLevel).GetLevel();
    }

    unsigned int getID() const { return m_id; }
    void setID(int id) { m_id = id; }

    float getUserRating() const { return m_userrating; }
    void setUserRating(float userRating) { m_userrating = userRating; }

    ////////////////////////////////

    void dumpToDatabase();
    void updateDatabase();

    bool deleteFile();
    bool dropFromDB();

    void Reset();

  private:
    void fillGenres();
    void updateGenres();
    bool removeDir(const QString &dirName);
    void fromDBRow(MSqlQuery &query);
    void saveToDatabase();

  private:
    QString m_url;
    QString m_title;
    QString m_inetref;
    QString m_series;
    QString m_description;
    QString m_rating;
    QString m_playcommand;
    genre_list m_genres;
    QString m_filename;
    QString m_coverfile;

    ParentalLevel::Level m_showlevel;
    unsigned int m_id;  // programmemetadata.intid
    float m_userrating;

    // not in DB
    Metadata::SortKey m_sort_key;
    QString m_prefix;
    int m_flat_index;
};

/////////////////////////////
/////////
/////////////////////////////
bool MetadataImp::removeDir(const QString &dirName)
{
    QDir d(dirName);

    const QFileInfoList *contents = d.entryInfoList();
    if (!contents)
    {
        return d.rmdir(dirName);
    }

    const QFileInfoListIterator it(*contents);
    QFileInfo *fi;

    while ((fi = it.current()) != 0)
    {
        if (fi->fileName() == "." ||
            fi->fileName() == "..")
        {
            continue;
        }
        if (fi->isDir())
        {
            QString fileName = fi->fileName();
            if (!removeDir(fileName))
                return false;
        }
        else
        {
            if (!QFile(fi->fileName()).remove())
                return false;
        }
    }
    return d.rmdir(dirName);
}

/// Deletes the file associated with a metadata entry
bool MetadataImp::deleteFile()
{
    bool isremoved = false;
    QFileInfo fi(m_filename);
    if (fi.isDir())
    {
        isremoved = removeDir(m_filename);
    }
    else
    {
        QFile programmefile;
        programmefile.setName(m_filename);
        isremoved = programmefile.remove();
    }

    if (!isremoved)
    {
        VERBOSE(VB_IMPORTANT, QString("impossible de supprimer le fichier"));
    }

    return isremoved;
}

bool MetadataImp::dropFromDB()
{
    ProgrammeGenreMap::getGenreMap().remove(m_id);

    MSqlQuery query(MSqlQuery::InitCon());
    query.prepare("DELETE FROM iplayer_metadata WHERE intid = :ID");
    query.bindValue(":ID", m_id);
    if (!query.exec())
    {
        MythContext::DBError("delete from iplayer_metadata", query);
    }

    return true;
}

void MetadataImp::Reset()
{

    MetadataImp tmp(m_url,m_filename, PROGRAMME_COVERFILE_DEFAULT,
                    Metadata::FilenameToTitle(m_filename),
                    PROGRAMME_INETREF_DEFAULT,
                    PROGRAMME_DESCRIPTION_DEFAULT, PROGRAMME_SERIES_DEFAULT, 0.0, PROGRAMME_RATING_DEFAULT, m_id,
                    ParentalLevel::plLowest, "",
                    Metadata::genre_list());
    tmp.m_prefix = m_prefix;
    tmp.m_flat_index = m_flat_index;

    *this = tmp;
}

void MetadataImp::fillGenres()
{
    m_genres.clear();
    ProgrammeGenreMap &vgm = ProgrammeGenreMap::getGenreMap();
    ProgrammeGenreMap::entry genres;
    if (vgm.get(m_id, genres))
    {
        ProgrammeGenre &vg = ProgrammeGenre::getGenre();
        for (ProgrammeGenreMap::entry::values_type::const_iterator p =
             genres.values.begin(); p != genres.values.end(); ++p)
        {
            // Just add empty string for no-name genres
            QString name;
            vg.get(*p, name);
            m_genres.push_back(genre_list::value_type(*p, name));
        }
    }
}


/// Sets metadata from a DB row
void MetadataImp::fromDBRow(MSqlQuery &query)
{
    m_url = QString::fromUtf8(query.value(11).toString());
    m_title = QString::fromUtf8(query.value(0).toString());
    m_description = QString::fromUtf8(query.value(1).toString());
    m_series = QString::fromUtf8(query.value(2).toString());
    m_rating = query.value(3).toString();
    m_userrating = (float)query.value(4).toDouble();
    if (isnan(m_userrating))
        m_userrating = 0.0;
    if (m_userrating < -10.0 || m_userrating >= 10.0)
        m_userrating = 0.0;
    m_filename = QString::fromUtf8(query.value(5).toString());
    m_showlevel = ParentalLevel(query.value(6).toInt()).GetLevel();
    m_coverfile = QString::fromUtf8(query.value(7).toString());
    m_inetref = QString::fromUtf8(query.value(8).toString());
    m_id = query.value(9).toInt();
    m_playcommand = query.value(10).toString();

    // Genres
    fillGenres();

}

void MetadataImp::saveToDatabase()
{
    if (m_title == "")
        m_title = Metadata::FilenameToTitle(m_filename);
    if (m_url == "")
        m_url = Metadata::FilenameToTitle(m_filename);
    if (m_series == "")
        m_series = PROGRAMME_SERIES_UNKNOWN;
    if (m_description == "")
        m_description = PROGRAMME_DESCRIPTION_DEFAULT;
    if (m_rating == "")
        m_rating = PROGRAMME_RATING_DEFAULT;
    if (m_coverfile == "")
        m_coverfile = PROGRAMME_COVERFILE_DEFAULT;
    if (m_inetref == "")
        m_inetref = PROGRAMME_INETREF_DEFAULT;
    if (isnan(m_userrating))
        m_userrating = 0.0;
    if (m_userrating < -10.0 || m_userrating >= 10.0)
        m_userrating = 0.0;

    bool inserting = m_id == 0;

    MSqlQuery query(MSqlQuery::InitCon());

    if (inserting)
    {

        query.prepare("INSERT INTO iplayer_metadata (title,description,series,"
                      "rating,userrating,filename,showlevel,"
                      "coverfile,inetref,url) VALUES (:TITLE, :DESCRIPTION, "
                      ":SERIES, :RATING, :USERRATING, "
                      ":FILENAME, :SHOWLEVEL, :COVERFILE, :INETREF, :URL)");

    }
    else
    {
        query.prepare("UPDATE iplayer_metadata SET title = :TITLE, "
                      "description = :DESCRIPTION, series = :SERIES, rating= :RATING, "
                      "userrating = :USERRATING, filename = :FILENAME, "
                      "showlevel = :SHOWLEVEL, coverfile = :COVERFILE, "
                      "inetref = :INETREF, "
                      "playcommand = :PLAYCOMMAND, url = :URL "
                      " WHERE intid = :INTID");

        query.bindValue(":PLAYCOMMAND", m_playcommand.utf8());
        query.bindValue(":INTID", m_id);
    }

    query.bindValue(":TITLE", m_title.utf8());
    query.bindValue(":DESCRIPTION", m_description.utf8());
    query.bindValue(":SERIES", m_series.utf8());
    query.bindValue(":RATING", m_rating.utf8());
    query.bindValue(":USERRATING", m_userrating);
    query.bindValue(":FILENAME", m_filename.utf8());
    query.bindValue(":SHOWLEVEL", m_showlevel);
    query.bindValue(":COVERFILE", m_coverfile.utf8());
    query.bindValue(":INETREF", m_inetref.utf8());
    query.bindValue(":URL", m_url);

    if (!query.exec() || !query.isActive())
    {
        MythContext::DBError("programme metadata update", query);
        return;
    }

    if (inserting)
    {
        // Must make sure we have 'id' filled before we call updateGenres or
        // updateCountries
        query.exec("SELECT LAST_INSERT_ID()");

        if (!query.isActive() || query.size() < 1)
        {
            MythContext::DBError("metadata id get", query);
            return;
        }

        query.next();
        m_id = query.value(0).toUInt();

        if (0 == m_id)
        {
            VERBOSE(VB_IMPORTANT,
                    QString("%1: The id of the last inserted row to "
                            "iplayer_metadata seems to be 0. This is odd.")
                    .arg(__FILE__));
            return;
        }
    }

    updateGenres();

}

void MetadataImp::dumpToDatabase()
{
    saveToDatabase();
}

void MetadataImp::updateDatabase()
{
    saveToDatabase();
}

void MetadataImp::updateGenres()
{
    ProgrammeGenreMap::getGenreMap().remove(m_id);

    // ensure that all genres we have are in the DB
    genre_list::iterator genre = m_genres.begin();
    while (genre != m_genres.end())
    {
        if (genre->second.stripWhiteSpace().length())
        {
            genre->first = ProgrammeGenre::getGenre().add(genre->second);
            ProgrammeGenreMap::getGenreMap().add(m_id, genre->first);
            ++genre;
        }
        else
        {
            genre = m_genres.erase(genre);
        }
    }
}

////////////////////////////////////////
//// Metadata
////////////////////////////////////////
Metadata::SortKey Metadata::GenerateDefaultSortKey(const Metadata &m,
                                                   bool ignore_case)
{
    QString title(ignore_case ? m.Title().lower() : m.Title());
    title = trimTitle(title, ignore_case);

    return SortKey(SortData(title, m.Filename(),
                         QString().sprintf("%.7d", m.ID())));
}

namespace
{
    QString eatBraces(const QString &title, const QString &left_brace,
                      const QString &right_brace)
    {
        QString ret(title);
        bool keep_checking = true;

        while (keep_checking)
        {
            int left_position = ret.find(left_brace);
            int right_position = ret.find(right_brace);
            if (left_position == -1 || right_position == -1)
            {
                //
                //  No matching sets of these braces left.
                //

                keep_checking = false;
            }
            else
            {
                if (left_position < right_position)
                {
                    //
                    //  We have a matching set like:  (  foo  )
                    //

                    ret = ret.left(left_position) +
                            ret.right(ret.length() - right_position - 1);
                }
                else if (left_position > right_position)
                {
                    //
                    //  We have a matching set like:  )  foo  (
                    //

                    ret = ret.left(right_position) +
                            ret.right(ret.length() - left_position - 1);
                }
            }
        }

        return ret;
    }
}

QString Metadata::FilenameToTitle(const QString &file_name)
{
    QString title = file_name.right(file_name.length() -
                                    file_name.findRev("/") - 1);
    title.replace(QRegExp("_"), " ");
    title.replace(QRegExp("%20"), " ");
    title = title.left(title.findRev("."));
    title.replace(QRegExp("\\."), " ");

    title = eatBraces(title, "[", "]");
    title = eatBraces(title, "(", ")");
    title = eatBraces(title, "{", "}");

    return title.stripWhiteSpace();
}

namespace
{
    const QRegExp &getTitleTrim(bool ignore_case)
    {
        static QString pattern(QObject::tr("^(The |A |An )"));
        static QRegExp prefixes_case(pattern, true);
        static QRegExp prefixes_nocase(pattern, false);
        return ignore_case ? prefixes_nocase : prefixes_case;
    }
}

QString Metadata::trimTitle(const QString &title, bool ignore_case)
{
    QString ret(title);
    ret.remove(getTitleTrim(ignore_case));
    return ret;
}

QString Metadata::getPlayer(const Metadata *item)
{
    QString empty;
    return getPlayer(item, empty);
}

QString Metadata::getPlayer(const Metadata *item, QString &internal_mrl)
{
    if (!item) return "";
    internal_mrl = item->URL();
    return gContext->GetSetting("mythiplayer.iPlayerCommand");
}

QString Metadata::getPlayCommand(const Metadata *item)
{
    if (!item) return "";

    QString filename = item->URL();
    QString handler = getPlayer(item);

    QString esc_fname =
            QString(item->URL()).replace(QRegExp("\""), "\\\"");
    QString arg = QString("\"%1\"").arg(esc_fname);

    QString command = "";

    // If handler contains %d, substitute default player command
    // This would be used to add additional switches to the default without
    // needing to retype the whole default command.  But, if the
    // command and the default command both contain %s, drop the %s from
    // the default since the new command already has it
    //
    // example: default: mplayer -fs %s
    //          custom : %d -ao alsa9:spdif %s
    //          result : mplayer -fs -ao alsa9:spdif %s
    if (handler.contains("%d"))
    {
        QString default_handler = gContext->GetSetting("ProgrammeDefaultPlayer");
        if (handler.contains("%s") && default_handler.contains("%s"))
        {
            default_handler = default_handler.replace(QRegExp("%s"), "");
        }
        command = handler.replace(QRegExp("%d"), default_handler);
    }

    if (handler.contains("%s"))
    {
        command = handler.replace(QRegExp("%s"), arg);
    }
    else
    {
        command = handler + " " + arg;
    }

    return command;
}

Metadata::Metadata(
              const QString &url,
              const QString &filename,
              const QString &coverfile,
              const QString &title,
              const QString &inetref,
              const QString &description,
              const QString &series,
              float userrating,
              const QString &rating,
              int id,
              ParentalLevel::Level showlevel,
              const QString &playcommand,
              const genre_list &genres)
{
    m_imp = new MetadataImp(url,filename, coverfile, title, inetref, description,
                            series, userrating, rating, id, showlevel,
                            playcommand, genres);
}

Metadata::~Metadata()
{
    delete m_imp;
}

Metadata::Metadata(MSqlQuery &query)
{
    m_imp = new MetadataImp(query);
}

Metadata::Metadata(const Metadata &rhs)
{
    *this = rhs;
}

Metadata &Metadata::operator=(const Metadata &rhs)
{
    if (this != &rhs)
    {
        m_imp = new MetadataImp(*(rhs.m_imp));
    }

    return *this;
}

bool Metadata::hasSortKey() const
{
    return m_imp->hasSortKey();
}

const Metadata::SortKey &Metadata::getSortKey() const
{
    return m_imp->getSortKey();
}

void Metadata::setSortKey(const Metadata::SortKey &sort_key)
{
    m_imp->setSortKey(sort_key);
}

void Metadata::setFlatIndex(int index)
{
    m_imp->setFlatIndex(index);
}

int Metadata::getFlatIndex() const
{
    return m_imp->getFlatIndex();
}


const QString &Metadata::getPrefix() const
{
    return m_imp->getPrefix();
}

void Metadata::setPrefix(const QString &prefix)
{
    m_imp->setPrefix(prefix);
}

const QString &Metadata::URL() const
{
    return m_imp->getURL();
}

void Metadata::setURL(const QString &url)
{
    m_imp->setURL(url);
}

const QString &Metadata::Title() const
{
    return m_imp->getTitle();
}

void Metadata::setTitle(const QString &title)
{
    m_imp->setTitle(title);
}

const QString &Metadata::InetRef() const
{
    return m_imp->getInetRef();
}

void Metadata::setInetRef(const QString &inetRef)
{
    m_imp->setInetRef(inetRef);
}

const QString &Metadata::Description() const
{
    return m_imp->getDescription();
}

void Metadata::setDescription(const QString &description)
{
    m_imp->setDescription(description);
}

const QString &Metadata::Series() const
{
    return m_imp->getSeries();
}

void Metadata::setSeries(const QString &series)
{
    m_imp->setSeries(series);
}

float Metadata::UserRating() const
{
    return m_imp->getUserRating();
}

void Metadata::setUserRating(float userRating)
{
    m_imp->setUserRating(userRating);
}

const QString &Metadata::Rating() const
{
    return m_imp->getRating();
}

void Metadata::setRating(const QString &rating)
{
    m_imp->setRating(rating);
}

unsigned int Metadata::ID() const
{
    return m_imp->getID();
}

void Metadata::setID(int id)
{
    m_imp->setID(id);
}

const QString &Metadata::PlayCommand() const
{
    return m_imp->getPlayCommand();
}

void Metadata::setPlayCommand(const QString &playCommand)
{
    m_imp->setPlayCommand(playCommand);
}


ParentalLevel::Level Metadata::ShowLevel() const
{
    return m_imp->getShowLevel();
}

void Metadata::setShowLevel(ParentalLevel::Level showLevel)
{
    m_imp->setShowLevel(showLevel);
}

const QString &Metadata::Filename() const
{
    return m_imp->getFilename();
}

void Metadata::setFilename(const QString &filename)
{
    m_imp->setFilename(filename);
}

QString Metadata::getFilenameNoPrefix() const
{
    return m_imp->getFilenameNoPrefix();
}

const QString &Metadata::CoverFile() const
{
    return m_imp->getCoverFile();
}

void Metadata::setCoverFile(const QString &coverFile)
{
    m_imp->setCoverFile(coverFile);
}

const Metadata::genre_list &Metadata::Genres() const
{
    return m_imp->getGenres();
}

void Metadata::setGenres(const genre_list &genres)
{
    m_imp->setGenres(genres);
}

void Metadata::dumpToDatabase()
{
    m_imp->dumpToDatabase();
}

void Metadata::updateDatabase()
{
    m_imp->updateDatabase();
}

#if 0
bool Metadata::fillDataFromID(const MetadataListManager &cache)
{
    if (m_imp->getID() == 0)
        return false;

    MetadataListManager::MetadataPtr mp = cache.byID(m_imp->getID());
    if (mp.get())
    {
        *this = *mp;
        return true;
    }

    return false;
}
#endif

bool Metadata::fillDataFromFilename(const MetadataListManager &cache)
{
    if (m_imp->getFilename() == "")
        return false;

    MetadataListManager::MetadataPtr mp =
            cache.byFilename(m_imp->getFilename());
    if (mp)
    {
        *this = *mp;
        return true;
    }

    return false;
}

bool Metadata::deleteFile()
{
    return m_imp->deleteFile();
}

bool Metadata::dropFromDB()
{
    return m_imp->dropFromDB();
}

void Metadata::Reset()
{
    m_imp->Reset();
}

bool operator==(const Metadata& a, const Metadata& b)
{
    if (a.Filename() == b.Filename())
        return true;
    return false;
}

bool operator!=(const Metadata& a, const Metadata& b)
{
    if (a.Filename() != b.Filename())
        return true;
    return false;
}

bool operator<(const Metadata::SortKey &lhs, const Metadata::SortKey &rhs)
{
    if (lhs.m_sd && rhs.m_sd)
        return *lhs.m_sd < *rhs.m_sd;
    else
    {
        VERBOSE(VB_IMPORTANT, QString("Error: Bug, Metadata item with empty "
                                      "sort key compared"));
        return lhs.m_sd < rhs.m_sd;
    }
}
